package capimServer;

import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Timer;



/**
 * Class to send packets through the net
 * @author danielm
 *
 */
public class SendPackets implements Runnable {
	private int base,nextSeqNum,windowSize, conectionId, serverPort, expectedSeqNum;
	private InetAddress server;
	// Queue of packets not sended yet
	private PacketQueue packetQueue;
	private PacketQueue sendedPackets;
	Timer timer;
	private Thread runner; 
	
	
	
	/**
	 * Default constructor
	 * @param args A string[2] with the server ip address and the port on server
	 */
	public SendPackets() {
		packetQueue = new PacketQueue(20);
		this.setBase(1);
		this.setNextSeqNum(1);
		this.setWindowSize(10);
		this.setExpectedSeqNum(1);
		sendedPackets = new PacketQueue(20);
		this.timer = new Timer();
			
		if(runner == null){
			runner = new Thread(this);
			runner.start();
		}
	}
	
	
	/**
	 * Thread main
	 */
	@Override
	public void run(){
		// If the queue isn't empty
		if(!packetQueue.isEmpty()){
			SimplePacketFormat pkt = null;
			try {
				// Extract the package from queue
				pkt = packetQueue.remove();
			} catch (Exception e) {
				e.printStackTrace();
			}
			// And try to send again
			if(pkt != null) this.sendMessage(pkt.getIp(),pkt.getMessage());
		}
		// Wait 2 seconds to verify the queue
		try {
			Thread.sleep(2000);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}
	
	
	
	/**
	 * Add a message to be sent in the packet queue
	 * @param ip server ip address
	 * @param port server port address
	 * @param Message String to send
	 */
	public void queueMessage(InetAddress ip, String Message){
		SimplePacketFormat pack = new SimplePacketFormat(ip,Message);
		try {
			packetQueue.add(pack);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	
	
	
	
	/**
	 * Send the message through the network
	 * @param ip server ip address
	 * @param port server port address
	 * @param Message String to send
	 */
	public synchronized void sendMessage(InetAddress ip, String Message){
		if(this.getNextSeqNum() < this.getBase() + this.getWindowSize()){
			try {
				DatagramSocket socket = new DatagramSocket();
				byte[] sendData = new byte[1024];
				Message = "1"+this.getNextSeqNum()+":"+Message;
				System.out.println("Mensagem enviada: "+Message);
				sendData = Message.getBytes();
				
				// Debug only
				// System.out.println("Dados: "+sendData.toString()+" : "+sendData.length+" : "+ip+" : "+port);
				DatagramPacket sendPacket = new DatagramPacket(sendData,sendData.length,ip,CapimServer.getClientPort());
				socket.send(sendPacket);
				
				// Add packet to sended queue of packets
				sendedPackets.add(new SimplePacketFormat(this.getNextSeqNum(),this.getServer(),Message));
				
				if(this.getNextSeqNum()==this.getBase()){
					// start timer
					this.timer = new Timer();
					this.timer.schedule(new ReSendPkt(sendedPackets), 2000);
				}
				this.setNextSeqNum(this.getNextSeqNum()+1);
				
			} catch (SocketException e1) {
				e1.printStackTrace();
			} catch (IOException e2) {
				e2.printStackTrace();
			} catch (Exception e3) {
				// TODO Auto-generated catch block
				e3.printStackTrace();
			}
		} else { 
			// Try to enqueue the message
			try {
				SimplePacketFormat qpkt = new SimplePacketFormat(ip,Message);
				this.packetQueue.add(qpkt);
			} catch (Exception e) {
				System.out.println("Queue Full, try again later");
			}
		}
	}
	
	
	
	
	/**
	 * Send message without changing id
	 * @param Message
	 */
	public synchronized void reSendMessage(SimplePacketFormat message){
		try {
			DatagramSocket socket = new DatagramSocket();
			byte[] sendData = new byte[1024];
			System.out.println("Mensagem Reenviada: "+message.getMessage());
			sendData = message.getMessage().getBytes();
			
			// Debug only
			// System.out.println("Dados: "+sendData.toString()+" : "+sendData.length+" : "+ip+" : "+port);
			DatagramPacket sendPacket = new DatagramPacket(sendData,sendData.length,this.getServer(),this.getServerPort());
			socket.send(sendPacket);
			
			// Add packet to sended queue of packets
			sendedPackets.add(new SimplePacketFormat(this.getNextSeqNum(),this.getServer(),message.getMessage()));
			
			// If resending the base message, restart timer
			if(message.getPktId()==this.getBase()){
				// Start timer, wait 2 seconds
				this.timer = new Timer();
				this.timer.schedule(new ReSendPkt(sendedPackets), 2000);
			}
			
		} catch (SocketException e1) {
			e1.printStackTrace();
		} catch (IOException e2) {
			e2.printStackTrace();
		} catch (Exception e3) {
			e3.printStackTrace();
		}
	}
	
	
	
	
	/**
	 * Send an ACK message
	 * @param Message ACK message
	 */
	public synchronized void sendAck(InetAddress ip,String Message){
		try {
			DatagramSocket socket = new DatagramSocket();
			byte[] sendData = new byte[1024];
			System.out.println("ACK to Send: "+Message);
			sendData = Message.getBytes();
			
			// Debug only
			// System.out.println("Dados: "+sendData.toString()+" : "+sendData.length+" : "+ip+" : "+port);
			DatagramPacket sendPacket = new DatagramPacket(sendData,sendData.length,ip,CapimServer.getClientPort());
			socket.send(sendPacket);
			
		} catch (SocketException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
}
	
	
	/**
	 * Analize a received packet, to see if they match with what are esperated
	 * and update the internal variables
	 * @param rdata String with received packet
	 */
	public synchronized void analizeReceivedPacket(String rdata){
		StringBuilder ackFrom = new StringBuilder();
		for(int i = 1; i< rdata.length() ; i++){
			if(rdata.charAt(i) == ':'){
				break;
			} else {
				ackFrom.append(rdata.charAt(i));
			}
		}
		int ackNum = Integer.parseInt(new String(ackFrom.toString().trim()));
		// Base Increment
		this.setBase(ackNum+1);
		
		// Now check if we need to cancel timer
		if(this.getBase() == this.getNextSeqNum()){
			this.timer.cancel();
		}
		// Now verify if the package from this ack still in the sendedPacket queue
		SimplePacketFormat tempPkt;
		while(true){
			try {
				if(this.sendedPackets.isEmpty()) break;
				tempPkt = this.sendedPackets.remove();
				// If i removed, break
				if(tempPkt.getPktId() == ackNum) break;
				else{
					// if is a duplicated ACK, just ignore
					if(tempPkt.getPktId() > ackNum) break;
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		// Debug only
		// System.out.println("Ack n: "+ackFrom);
		// System.out.println("New base: "+this.getBase());
	}
	
	
	
	public int getServerPort() {
		return serverPort;
	}

	public void setServerPort(int serverPort) {
		this.serverPort = serverPort;
	}

	public InetAddress getServer() {
		return server;
	}

	public void setServer(InetAddress server) {
		this.server = server;
	}
	
	public void setBase(int base) {
		this.base = base;
	}

	public int getBase() {
		return base;
	}

	public void setNextSeqNum(int nexSeqNum) {
		this.nextSeqNum = nexSeqNum;
	}

	public int getNextSeqNum() {
		return nextSeqNum;
	}

	public void setWindowSize(int windowSize) {
		this.windowSize = windowSize;
	}

	public int getWindowSize() {
		return windowSize;
	}

	public void setConectionId(int conectionId) {
		this.conectionId = conectionId;
	}

	public int getConectionId() {
		return conectionId;
	}
	
	public int getExpectedSeqNum() {
		return expectedSeqNum;
	}

	public void setExpectedSeqNum(int expectedSeqNum) {
		this.expectedSeqNum = expectedSeqNum;
	}

}
